package com.ludomancy.gamelib;

import java.io.IOException;
import java.util.Properties;
import java.util.Date;
import java.applet.Applet;
import netscape.javascript.*;

import java.util.StringTokenizer;
import java.util.Vector;
import java.util.Enumeration;

/**
 * This class provides an interface for reading & writing an Applet's state.
 * <p>
 * The state is represented as a Properties object which is a map of key,value
 * string pairs. The Properties object is saved as a cookie in the browser's
 * local cookies cache.
 * <p>
 * For limitations of saving data in cookies see related documentation.
 * <p>
 * Both the key and value strings in each pair must not include the following
 * characters:
 * <p>
 * ',' (comma) ';' (semi colon) and '=' (equality sign).
 * <p>
 * NOTE: Cookie names are case sensitive
 * 
 * @see java.util.Properties
 * @author Yoav Zur & Amit Caspi
 */
class DataUtil
{
	public class DataUtilCookieNotFoundException extends IOException
	{
		/**
		 * Create a new exception after not finding the given cookie
		 */
		public DataUtilCookieNotFoundException(String cookieName)
		{
			super("Cookie not found: " + cookieName);
		}
	}

	/**
	 * Thrown when trying to use CookiesJar in a browser that does not support
	 * cookies caching or if caching is disabled
	 * 
	 * @author Yoav Zur & Amit Caspi
	 */
	public class DataUtilCookieNotSupportedException extends IOException
	{
		private Exception exception;

		/**
		 * Create a new exception with the given message, wrapping the given
		 * internal exception
		 */
		public DataUtilCookieNotSupportedException(String message,
				Exception nestedException)
		{
			super(message);
			exception = nestedException;
		}
		
		/**
		 * Returns the internal (nested) exception that caused this exception
		 */
		public Exception getNestedException()
		{
			return exception;
		}
	}

	/**
	 * Thrown when failed to parse a cookie
	 * 
	 * @author Yoav Zur & Amit Caspi
	 */
	public class DataUtilCookieParseException extends java.io.IOException
	{
		/**
		 * Create a new exception after failing to parse the given cookie
		 */
		public DataUtilCookieParseException(String cookieName)
		{
			super("Failed to parse cookie: " + cookieName);
		}
	}

	private static final String COOKIE_NAME_PREFIX = "cookie";

	private static final char[] illegalChars =
	{ ';', ',', '=' };

	/**
	 * The current applet
	 */
	private Applet applet;

	/**
	 * Creates a cookies jar that handles cookies for the given applet
	 */
	public DataUtil(Applet applet)
	{
		this.applet = applet;
	}

	/**
	 * Returns an array of names of available cookies.
	 * <p>
	 * Returns an array with size 0 if none found
	 * 
	 * @throws DataUtilCookieNotSupportedException
	 *             if the browser does not support cookie caching or caching is
	 *             disabled.
	 */
	public String[] getAllNames() throws DataUtilCookieNotSupportedException
	{
		Vector namesVec = new Vector();
		try
		{
			String cookies = (String) JSObject.getWindow(applet).eval(
					"document.cookie");
			if (cookies != null)
			{
				StringTokenizer st = new StringTokenizer(cookies, ";", false);
				while (st.hasMoreTokens())
				{
					String cookie = st.nextToken().trim();
					namesVec.addElement(getCookieName(cookie));
				}
			}
			String[] namesArr = new String[namesVec.size()];
			namesVec.copyInto(namesArr);
			return namesArr;
		} catch (JSException h)
		{
			throw new DataUtilCookieNotSupportedException(
					"Failed to get cookie names", h);
		}
	}

	/**
	 * Returns true if a cookie with name = 'cookieName' exists otherwise
	 * returns false
	 * 
	 * @throws DataUtilCookieNotSupportedException
	 *             if the browser does not support cookie caching or caching is
	 *             disabled.
	 */
	public boolean isExist(String cookieName)
			throws DataUtilCookieNotSupportedException
	{
		try
		{
			String cookies = (String) JSObject.getWindow(applet).eval(
					"document.cookie");
			StringTokenizer st = new StringTokenizer(cookies, ";", false);
			while (st.hasMoreTokens())
			{
				String cookie = st.nextToken().trim();
				if (getCookieName(cookie).equals(cookieName))
				{
					return true;
				}
			}
			return false;
		} catch (JSException h)
		{
			throw new DataUtilCookieNotSupportedException(
					"Failed to compare cookies", h);
		}

	}

	/**
	 * Creates a new Properties object with the (key,value) pairs stored in the
	 * given cookie.
	 * <p>
	 * 
	 * @throws DataUtilCookieNotFoundException
	 *             if the cookie is not found
	 * @throws DataUtilCookieParseException
	 *             if the cookie can't be parsed
	 * @throws DataUtilCookieNotSupportedException
	 *             if the browser does not support cookie caching or caching is
	 *             disabled.
	 */
	public Properties read(String cookieName)
			throws DataUtilCookieParseException,
			DataUtilCookieNotFoundException,
			DataUtilCookieNotSupportedException
	{
		try
		{
			String cookies = (String) JSObject.getWindow(applet).eval(
					"document.cookie");
			if (cookies == null)
			{
				throw new DataUtilCookieNotFoundException(cookieName);
			}
			StringTokenizer st = new StringTokenizer(cookies, ";", false);
			while (st.hasMoreTokens())
			{
				String cookie = st.nextToken().trim();
				if (getCookieName(cookie).equals(cookieName))
				{
					try
					{
						return parseCookie(cookie);
					} catch (Exception j)
					{
						throw new DataUtilCookieParseException(cookieName);
					}
				}
			}
			throw new DataUtilCookieNotFoundException(cookieName);
		} catch (JSException h)
		{
			throw new DataUtilCookieNotSupportedException("Failed to read "
					+ cookieName, h);
		}
	}

	/**
	 * Writes the Properties object as a cookie into the browser's cache.
	 * <p>
	 * The cookie is saved under the given cookie name.
	 * <p>
	 * The browser will automatically delete the cookie at the given expire
	 * date.
	 * 
	 * @throws DataUtilCookieNotSupportedException
	 *             if the browser does not support cookie caching or caching is
	 *             disabled.
	 * @throws DataUtilCookieParseException
	 *             if a key or value inside the properties object is not legal
	 *             (see above).
	 */
	public void write(Properties props, String cookieName, Date expireDate)
			throws DataUtilCookieParseException,
			DataUtilCookieNotSupportedException
	{
		if (!this.validateString(cookieName))
		{
			throw new DataUtilCookieParseException(cookieName);
		}
		StringBuffer buffer = new StringBuffer("document.cookie ='");
		buffer.append(COOKIE_NAME_PREFIX);
		buffer.append(cookieName);
		buffer.append("=");
		Enumeration keys = props.keys();
		while (keys.hasMoreElements())
		{
			String key = (String) keys.nextElement();
			String value = props.getProperty(key);
			if (!this.validateString(key) || !this.validateString(value))
			{
				throw new DataUtilCookieParseException(cookieName);
			}
			buffer.append(key);
			buffer.append(",");
			buffer.append(value);
			if (keys.hasMoreElements())
			{
				buffer.append(",");
			}
		}
		buffer.append(";expires=");

		try
		{
			buffer.append(getUTCString(expireDate));
			buffer.append("'");
			JSObject.getWindow(applet).eval(buffer.toString());
		} catch (Exception j)
		{
			throw new DataUtilCookieNotSupportedException("Failed to write: "
					+ cookieName, j);
		}
	}

	/**
	 * This method returns the cookie name without the prefix.
	 * 
	 * @param cookieStr
	 *            the whole cookie string
	 */
	private String getCookieName(String cookieStr)
	{
		int index = cookieStr.lastIndexOf('=');
		return cookieStr.substring(COOKIE_NAME_PREFIX.length(), index);
	}

	private Properties parseCookie(String cookieStr)
	{
		Properties props = new Properties();
		int index = cookieStr.lastIndexOf('=');
		String data = cookieStr.substring(index + 1, cookieStr.length());
		StringTokenizer st = new StringTokenizer(data, ",", false);
		while (st.hasMoreTokens())
		{
			String key = st.nextToken().trim();
			String value = st.nextToken().trim();
			props.put(key, value);
		}
		return props;
	}

	/**
	 * Convert this date to the string form that JavaScript understands
	 */
	private String getUTCString(Date date) throws JSException
	{
		JSObject jsdate = (JSObject) JSObject.getWindow(applet)
				.eval("new Date");
		jsdate.call("setDate", new Object[]
		{ new Integer(date. getDate()) });
		jsdate.call("setMonth", new Object[]
		{ new Integer(date.getMonth()) });
		jsdate.call("setYear", new Object[]
		{ new Integer(date.getYear() + 1900) });
		return ((String) jsdate.call("toGMTString", new Object[] {}));
	}

	/**
	 * returns true iff the string is a valid key or value we can insert into
	 * the cookie.
	 */
	private boolean validateString(String str)
	{
		for (int i = 0; i < illegalChars.length; i++)
		{
			if (str.indexOf(illegalChars[i]) >= 0)
			{
				return false;
			}
		}
		return true;
	}
}